Skip to content

fix: keep refreshing after markets close - #22

Merged
TerrifiedBug merged 1 commit into
masterfrom
fix/refresh-stops-after-market-close
Aug 16, 2026
Merged

fix: keep refreshing after markets close#22
TerrifiedBug merged 1 commit into
masterfrom
fix/refresh-stops-after-market-close

Conversation

@TerrifiedBug

Copy link
Copy Markdown
Owner

The bug

Reported symptom: the menu bar showed a price from yesterday at 3pm.

Evidence from the live 1.5.0 install:

Process start Sat Aug 15 15:05:51 — a Saturday, US markets shut
Elapsed 18 h 24 m
Total CPU 0.30 s — the single startup fetch, and nothing since
rotationSpeed 30 — and the status item repainted at exactly :15/:45, every 30 s

The rotation timer had survived all 44 sleep/wake cycles, so this was never a dead timer. The refresh timer was firing every 60 s and returning immediately:

if isTimerTriggered && !anyMarketActive { return }

anyMarketActive is derived from the cached marketState, and only a fetch can update marketState — so the gate blocked the one thing that could clear it. The first closed session latched refreshes off for the rest of the process lifetime. It would not have resumed when the US market reopened on Monday.

This fires every single day, not just at weekends: once Yahoo moves from POSTPOST to CLOSED, TickerBar is frozen until it is relaunched.

The fix

A closed market must slow the cadence, never stop it. The repeating timer plus skip-gate becomes a one-shot timer that re-arms itself after every fetch, so the cadence always reflects the state the last fetch actually observed and marketState is always re-read.

  • Active session → the user's refresh interval.
  • Everything closedmax(interval, 15 min). 96 fetches a day, and a reopening, holiday or half-day is picked up within 15 minutes with no relaunch.
  • Fetch failed → retried after 5 s, 15 s, 60 s, capped by the cadence, then the cadence takes back over so an outage cannot become a hot loop.

Also fixed, since "always refreshing" needs them:

  • Wake from sleep refreshes immediately via NSWorkspace.didWakeNotification and re-anchors the cadence, instead of showing pre-sleep prices until the next tick.
  • Watchlist panel tops up quotes older than one interval as it appears.
  • Both timers moved to .common run loop modes, so a refresh still lands while the panel is open — the menu-tracking run loop does not service .default-only timers.
  • Both timers carry a tolerance, letting macOS coalesce their wakeups with other timers instead of waking the CPU alone. Correctness comes from re-arming after each fetch, not from firing on an exact second.
  • Concurrent callers (timer, wake, panel, manual button) coalesce onto one in-flight fetch, so a wake landing on an overdue timer cannot double-fetch or double-fire price alerts.

Verification

  • 6 new tests, 91 total, all passing. They pin the contract that a closed market yields a finite cadence rather than a skipped fetch, that the user's interval always wins when slower than the idle cadence, that an enabled extended session keeps the fast cadence, and that the backoff never retries slower than the cadence and expires back to it.
  • Widget extension target builds (the other half of the CI gate).
  • Smoke tested: ran the fixed build on a Sunday with every market closed and it fetched live data (SRAD $13.82 +1.3%) — under 1.5.0 the same state was the frozen one.

Timer refreshes were skipped whenever no watchlist session was active, but
that gate read the marketState cached by the last fetch and only a fetch
could update it. The first closed session latched the app off for the rest
of its life: it never saw the market reopen, so the menu bar kept showing a
price from hours or days earlier until TickerBar was relaunched.

Replace the repeating timer plus skip-gate with a one-shot timer that
re-arms itself after every fetch. A closed market now only stretches the
cadence to 15 minutes, so marketState is always re-read and a reopening,
holiday or half-day is always noticed. Refresh on wake, top up a stale
panel as the watchlist appears, and retry a failed fetch after 5, 15 and
60 seconds before falling back to the cadence. Both timers move to the
common run loop modes so they keep ticking while the panel is open, and
both carry a tolerance so macOS can coalesce their wakeups.
@TerrifiedBug
TerrifiedBug merged commit 37253f3 into master Aug 16, 2026
1 check passed
@TerrifiedBug
TerrifiedBug deleted the fix/refresh-stops-after-market-close branch August 16, 2026 09:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant